package org.will.http.service.impl;

import org.will.http.CBHttpLogFactory;
import org.will.http.domain.CBHttpConfig;
import org.will.http.domain.CBHttpCredential;
import org.will.http.domain.CBHttpHeaderName;
import org.will.http.domain.CBHttpResult;
import org.will.http.service.ICBHttpHandler;
import org.will.http.util.CBHttpUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.EntityBuilder;
import org.apache.http.client.entity.GzipDecompressingEntity;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;

import javax.net.ssl.SSLContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Author wangwei-ww
 * @Date 2017/3/27 19:24
 * @Comment
 */
public class CBDefaultHttpHander implements ICBHttpHandler {
    private Logger logger = CBHttpLogFactory.getLogger();
    private CloseableHttpClient httpclient;
    private IdleConnectionMonitorThread monitorThread;
    private CBHttpConfig httpConfig;
    private Long measureCost = Long.MAX_VALUE;
    private ResponseHandler<CBHttpResult> resHandler = new ResponseHandler<CBHttpResult>() {
        @Override
        public CBHttpResult handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
            CBHttpResult result = new CBHttpResult();
            result.setStatusCode(response.getStatusLine().getStatusCode());
            Header[] headers = response.getAllHeaders();
            handleResHandler(result, headers);
            HttpEntity resEntity = null;
            if (result.isGzipEncoding()) {
                resEntity = new GzipDecompressingEntity(response.getEntity());
            } else {
                resEntity = response.getEntity();
            }

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            resEntity.writeTo(os);
            result.setRawData(os.toByteArray());
            return result;
        }
    };

    public CBDefaultHttpHander(final CBHttpConfig httpConfig) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        this.httpConfig = httpConfig;
        PoolingHttpClientConnectionManager cm = getConnectionManager(httpConfig);
        HttpClientBuilder httpBuilder = HttpClients.custom();
        CredentialsProvider defaultCredentialsProvider = new BasicCredentialsProvider();
        httpBuilder = setProxy(httpConfig, httpBuilder, defaultCredentialsProvider);
        setCredential(httpConfig, defaultCredentialsProvider);
        httpBuilder.setDefaultCredentialsProvider(defaultCredentialsProvider);
        httpBuilder = httpBuilder
                .setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(httpConfig.getConnectionTimeout())
                        .setSocketTimeout(httpConfig.getReadTimeout()).build());
        ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                return httpConfig.getKeepaliveTimeout();
            }
        };
        if (!httpConfig.isAutoRetry()) {
            HttpRequestRetryHandler retryHandler = new HttpRequestRetryHandler() {
                public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                    return false;
                }
            };
            httpBuilder.setRetryHandler(retryHandler);
        }
        httpBuilder.setKeepAliveStrategy(myStrategy);
        httpclient = httpBuilder.build();
        httpclient = httpBuilder.setConnectionManager(cm).build();
        monitorThread = new IdleConnectionMonitorThread(cm, httpConfig.getMaxIdleTime());
        monitorThread.start();
    }

    private HttpClientBuilder setProxy(CBHttpConfig httpConfig, HttpClientBuilder httpBuilder,
                                       CredentialsProvider defaultCredentialsProvider) {
        if (httpConfig.getProxy() != null) {
            httpBuilder = httpBuilder
                    .setProxy(new HttpHost(httpConfig.getProxy().getHost(), httpConfig.getProxy().getPort()));

            if (httpConfig.getProxy().isAuthProvided()) {
                defaultCredentialsProvider.setCredentials(
                        new AuthScope(httpConfig.getProxy().getHost(), httpConfig.getProxy().getPort()),
                        new UsernamePasswordCredentials(httpConfig.getProxy().getUsername(),
                                httpConfig.getProxy().getPassword()));
            }
        }
        return httpBuilder;
    }

    private void setCredential(CBHttpConfig httpConfig, CredentialsProvider defaultCredentialsProvider) {
        if (httpConfig.getCredentials() != null) {
            for (CBHttpCredential credentail : httpConfig.getCredentials()) {
                defaultCredentialsProvider.setCredentials(new AuthScope(credentail.getHost(), credentail.getPort()),
                        new UsernamePasswordCredentials(credentail.getUsername(), credentail.getPassword()));
            }
        }
    }

    private void setHeader(Map<String, String> headers, HttpMessage httpMethod) {
        if (headers != null) {
            Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator();
            for (; it.hasNext(); ) {
                Map.Entry<String, String> entry = it.next();
                httpMethod.addHeader(entry.getKey(), entry.getValue());
            }
        }
    }

    private CBHttpResult executeHttp(Map<String, String> headers, HttpClientContext context, HttpUriRequest httpMethod) {
        setHeader(headers, httpMethod);
        CBHttpResult result = null;
        try {
            result = httpclient.execute(httpMethod, resHandler, context);
        } catch (IOException e) {
            logger.error("http execute", e);
            result = new CBHttpResult();
            result.setRawData(e.getMessage().getBytes());
            result.setIoException(e);
        }
        return result;
    }

    @Override
    public CBHttpResult get(String url, Map<String, String> headers) {
        CBHttpResult result = null;
        HttpClientContext context = HttpClientContext.create();
        HttpGet httpget = new HttpGet(url);
        result = executeHttp(headers, context, httpget);
        return result;
    }

    @Override
    public CBHttpResult post(String url, Map<String, String> headers, Map<String, Object> params) {
        CBHttpResult result = null;
        HttpClientContext context = HttpClientContext.create();
        HttpPost httpPost = new HttpPost(url);
        List<NameValuePair> postParameters = new ArrayList<NameValuePair>();
        Iterator<Map.Entry<String, Object>> it = params.entrySet().iterator();
        for (; it.hasNext(); ) {
            Map.Entry<String, Object> entry = it.next();
            postParameters.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())));
        }
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(postParameters));
        } catch (UnsupportedEncodingException e) {
        }
        result = executeHttp(headers, context, httpPost);
        return result;
    }

    @Override
    public CBHttpResult postStream(String url, Map<String, String> headers, byte[] rawData) {
        CBHttpResult result = null;
        HttpClientContext context = HttpClientContext.create();
        HttpPost httpPost = new HttpPost(url);
        HttpEntity httpEntity = EntityBuilder.create().setBinary(rawData).build();
        httpPost.setEntity(httpEntity);
        executeHttp(headers, context, httpPost);
        return result;
    }

    @Override
    public CBHttpResult postJson(String url, Map<String, String> headers, String json) {
        HttpClientContext context = HttpClientContext.create();
        HttpPost httpPost = new HttpPost(url);
        HttpEntity httpEntity =
                EntityBuilder.create().setText(json).setContentType(ContentType.APPLICATION_JSON).build();
        httpPost.setEntity(httpEntity);
        return executeHttp(headers, context, httpPost);
    }

    @Override
    public CBHttpResult postMultiPart(String url, Map<String, String> headers, Map<String, Object> params) {
        return null;
    }

    @Override
    public void shutdown() {
        if (httpclient != null) {
            try {
                httpclient.close();
            } catch (IOException e) {
                logger.error("CBHttpHandler shutdown Exption:", e);
            }
        }
    }

    private PoolingHttpClientConnectionManager getConnectionManager(CBHttpConfig httpConfig) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.<ConnectionSocketFactory>create();
        SSLContext sslContext = SSLContexts.custom().useTLS().loadTrustMaterial(null, new TrustStrategy() {
            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                return true;
            }
        }).build();
        LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        registryBuilder.register("https", sslSocketFactory);
        registryBuilder.register("http", PlainConnectionSocketFactory.getSocketFactory());
        Registry<ConnectionSocketFactory> registry = registryBuilder.build();
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(registry);
        connManager.setDefaultMaxPerRoute(30);
        connManager.setMaxTotal(300);
        connManager.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(httpConfig.getReadTimeout()).build());
        return connManager;
    }

    private void handleResHandler(CBHttpResult result, Header[] headers) {
        String encode;
        if (headers != null) {
            for (Header header : headers) {
                result.addHeader(header.getName().toLowerCase(), header.getValue());
                if (CBHttpHeaderName.SET_COOKIE.getValue().equals(header.getName().toLowerCase())) {
                    String cookieTmp[] = header.getValue().split(";");
                    result.addSetCoocies(cookieTmp[0]);
                } else if (CBHttpHeaderName.CONTENT_TYPE.getValue().equalsIgnoreCase(header.getName())) {
                    encode = CBHttpUtil.getCharSet(header.getValue());
                    if (StringUtils.isNotBlank(encode)) {
                        result.setEncoding(encode);
                    }
                }
            }
        }
    }


    public static class IdleConnectionMonitorThread extends Thread {
        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;
        private int maxIdleTime;

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr, int maxIdleTime) {
            super();
            this.connMgr = connMgr;
            this.maxIdleTime = maxIdleTime;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5000);
                        connMgr.closeExpiredConnections();
                        connMgr.closeIdleConnections(maxIdleTime, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }

    @Override
    public Long getMeasureCost() {
        return this.measureCost;
    }

    @Override
    public void setMeasureCost(long cost) {
        this.measureCost = cost;
    }

    @Override
    public CBHttpConfig getHttpConfig() {
        return this.httpConfig;
    }
}
